# coding=utf-8
from __future__ import print_function, absolute_import
from gm.api import *
import datetime
import pandas as pd
import numpy as np

# 来源： https://bbs.myquant.cn/thread/3068

"""
R-Breaker下的可转债策略
1、先获取最近N个交易日最活跃的M只可转债（小市值因子+高波动因子，周频更换）；
2、M只可转债平均仓位按照R-Breaker策略进行交易（仅做多）；
3、每只标的每周仅交易一次，动态止盈止损，尾盘平仓。

R-Breaker策略解释说明：R-Breaker是一种短线日内交易策略。
策略根据前一个交易日的收盘价、最高价和最低价数据通过一定方式计算出六个价位，从大到小依次为：突破买入价、观察卖出价、反转卖出价、反转买入、观察买入价、突破卖出价。
以此来形成当前交易日盘中交易的触发条件，追踪盘中价格走势，实时判断触发条件。
具体条件如下：
突破
在空仓条件下，如果盘中价格超过突破买入价，则采取趋势策略，即在该点位开仓做多。
在空仓条件下，如果盘中价格跌破突破卖出价，则采取趋势策略，即在该点位开仓做空。
反转
当日内最高价超过观察卖出价后，盘中价格出现回落，且进一步跌破反转卖出价构成的支撑线时，采取反转策略，开空仓或反手做空。
当日内最低价低于观察买入价后，盘中价格出现反弹，且进一步超过反转买入价构成的阻力线时，采取反转策略，开多仓或反手做多。
"""


def init(context):
    context.symbol = []
    # 设置最大持有数量
    context.max_holding_num = 5
    # 波动计算周期(天)
    context.volatility_periods = 5
    # 设置动态止盈止损比例
    context.stopLoss = 0.03
    # 开盘前处理函数
    schedule(schedule_func=before_trading, date_rule='1d', time_rule='09:00:00')


def before_trading(context):
    # 获取所有可转债
    symbols = get_normal_stocks(context.now)
    # 前N个交易日时间
    start_time = get_previous_N_trading_date(context.now,counts=context.volatility_periods+1)
    pre_date = get_previous_trading_date(exchange='SZSE', date=context.now)
    # 周初换股
    if datetime.datetime.strptime(pre_date,'%Y-%m-%d').weekday()>context.now.weekday(): 
        # 可转债正股字典
        conbond_dict = get_instrumentinfos(symbols=symbols,fields='symbol,underlying_symbol',df=True)
        stock2conbond = {conbond_dict.loc[index,'underlying_symbol']:conbond_dict.loc[index,'symbol'] for index in conbond_dict.index}
        # 获取所有股票市值,并按升序排序
        all_stock = stock2conbond.keys()
        fundamental = get_fundamentals_n('trading_derivative_indicator', ','.join(all_stock), pre_date, fields='TOTMKTCAP', count=1, df=True).sort_values(by='TOTMKTCAP').dropna()
        # 获取前2N只股票
        to_buy_stock = list(fundamental.iloc[:context.max_holding_num*2,:]['symbol'])
        context.symbol = [stock2conbond[stock] for stock in to_buy_stock]
        
        # 高波动选股
        # 获取历史数据
        close_price = history(symbol=','.join(context.symbol), frequency='1d', start_time=start_time, end_time=pre_date, fields='eob,symbol,close', skip_suspended=True, 
                fill_missing=None, adjust=ADJUST_PREV, adjust_end_time=context.backtest_end_time, df=True).set_index(['eob','symbol']).unstack()
        close_price.columns = close_price.columns.droplevel(level=0)
        # 计算N日波动
        volatility = (close_price/close_price.shift(1)-1).dropna().std().sort_values(ascending=False)
        # 选取波动率最大的N只标的
        context.symbol = list(volatility.iloc[:context.max_holding_num].index)

        if len(context.symbol)>0:
            # 获取R-breaker相关数据
            data = history(symbol=','.join(context.symbol), frequency='1d', start_time=pre_date, end_time=pre_date, fields='symbol,close,high,low', skip_suspended=True, 
                    fill_missing=None, adjust=ADJUST_PREV, adjust_end_time=context.backtest_end_time, df=True).set_index('symbol')
            data['pivot'] = (data['high']+data['low']+data['close'])/3# 枢轴点

            data['bBreak'] = data['high']+2*(data['pivot']-data['low'])# 突破买入价
            data['sSetup'] = data['pivot']+(data['high']-data['low'])# 观察卖出价
            data['sEnter'] = 2*data['pivot']-data['low']#反转卖出价
            data['bEnter'] = 2*data['pivot']-data['high']#反转买入价
            data['bSetup'] = data['pivot']-(data['high']-data['low'])# 观察买入价
            data['sBreak'] = data['low']-2*(data['high']-data['pivot'])# 突破卖出价
            context.data = data
            context.data.loc[:,'today_low'] = 9999# 日内最低价
            context.data.loc[:,'today_high'] = 0# 日内最高价
            context.data.loc[:,'buy_times'] = 0# 交易次数
            context.data.loc[:,'holding_high'] = 0# 买入持有后的最高价
            # 订阅数据
            subscribe(context.symbol, frequency='60s', count=1, wait_group=False, unsubscribe_previous=True)


def on_bar(context, bars):
    # 尾盘平仓
    if context.now.hour == 14 and context.now.minute >= 59 or context.now.hour == 15:
        order_close_all()

    # 非尾盘的交易逻辑
    else:
        code = bars[0]['symbol']
        new_price = bars[0]['close']
        context.data.loc[code,'today_low'] = min(context.data.loc[code,'today_low'],bars[0]['low']) if not np.isnan(bars[0]['low']) else context.data.loc[code,'today_low']
        context.data.loc[code,'today_high'] = max(context.data.loc[code,'today_high'],bars[0]['high']) if not np.isnan(bars[0]['high']) else context.data.loc[code,'today_high']
        # 获取现有持仓
        position_long = context.account().position(symbol=code, side=PositionSide_Long)# 多头仓位

        # 交易逻辑
        if not position_long and context.data.loc[code,'buy_times']<1: # 无持仓
            # 趋势买入
            if new_price > context.data.loc[code,'bBreak']:  
                # 在空仓的情况下，如果盘中价格超过突破买入价，则采取趋势策略，即在该点位开仓做多
                order_target_percent(symbol=code, percent=2/context.max_holding_num, position_side=PositionSide_Long, order_type=OrderType_Market)
                context.data.loc[code,'buy_price'] = new_price # 开仓价
                context.data.loc[code,'buy_times'] += 1
                context.data.loc[code,'holding_high'] = new_price

            # 反转买入
            if context.data.loc[code,'today_low'] < context.data.loc[code,'bSetup'] and new_price > context.data.loc[code,'bEnter']:
                # 当日内最低价低于观察买入价后，盘中价格出现反弹，且进一步超过反转买入价构成的阻力线时，采取反转策略，开多仓或反手做多，即买入
                order_target_percent(symbol=code, percent=2/context.max_holding_num, position_side=PositionSide_Long, order_type=OrderType_Market)
                context.data.loc[code,'buy_price'] = new_price # 开仓价
                context.data.loc[code,'today_high'] = new_price # 重置日内最高价，防止反转买入前最高价已超过观察卖出价，从而导致买入后立即被卖出
                context.data.loc[code,'buy_times'] += 1
                context.data.loc[code,'holding_high'] = new_price
        elif position_long:  # 有持仓
            context.data.loc[code,'holding_high'] =  max(context.data.loc[code,'holding_high'],bars[0]['high']) if not np.isnan(bars[0]['high']) else context.data.loc[code,'holding_high']
            # 止损（当前行情价格低于止损价时，卖出止损）
            if new_price<context.data.loc[code,'holding_high']*(1-context.stopLoss):
                order_target_percent(symbol=code, percent=0, position_side=PositionSide_Long, order_type=OrderType_Market)
                unsubscribe(symbols=code, frequency='60s')

            # 反转卖出:
            if context.data.loc[code,'today_high'] > context.data.loc[code,'sSetup'] and new_price < context.data.loc[code,'sEnter']:
                # 当日内最高价超过观察卖出价后，盘中价格出现回落，且进一步跌破反转卖出价构成的支撑线时，采取反转策略，开空仓或反手做空，即卖出
                order_target_percent(symbol=code, percent=0, position_side=PositionSide_Long, order_type=OrderType_Market)
                unsubscribe(symbols=code, frequency='60s')


def on_order_status(context, order):
    # 标的代码
    symbol = order['symbol']
    # 委托价格
    price = order['price']
    # 委托数量
    volume = order['volume']
    # 目标仓位
    target_percent = order['target_percent']
    # 查看下单后的委托状态，等于3代表委托全部成交
    status = order['status']
    # 买卖方向，1为买入，2为卖出
    side = order['side']
    # 开平仓类型，1为开仓，2为平仓
    effect = order['position_effect']
    # 委托类型，1为限价委托，2为市价委托
    order_type = order['order_type']
    if status == 3:
        if effect == 1:
            if side == 1:
                side_effect = '开多仓'
            elif side == 2:
                side_effect = '开空仓'
        else:
            if side == 1:
                side_effect = '平空仓'
            elif side == 2:
                side_effect = '平多仓'
        order_type_word = '限价' if order_type==1 else '市价'
        print('{}:标的：{}，操作：以{}{}，委托价格：{}，委托数量：{}'.format(context.now,symbol,order_type_word,side_effect,price,volume))


def get_previous_N_trading_date(date,counts=1,exchanges='SHSE'):
    """
    获取end_date前N个交易日,end_date为datetime格式，包括date日期
    :param date：目标日期
    :param counts：历史回溯天数，默认为1，即前一天
    """
    if isinstance(date,str) and len(date)>10:
        date = datetime.datetime.strptime(date,'%Y-%m-%d %H:%M:%S')
    if isinstance(date,str) and len(date)==10:
        date = datetime.datetime.strptime(date,'%Y-%m-%d')
    previous_N_trading_date = get_trading_dates(exchange=exchanges, start_date=date-datetime.timedelta(days=max(counts+30,counts*2)), end_date=date)[-counts]
    return previous_N_trading_date


def get_normal_stocks(date):
    """
    获取目标日期date的可转债代码
    :param date：目标日期
    """
    if isinstance(date,str) and len(date)==10:
        date = datetime.datetime.strptime(date,"%Y-%m-%d")
    elif isinstance(date,str) and len(date)>10:
        date = datetime.datetime.strptime(date,"%Y-%m-%d %H:%M:%S")
    date = date.date()
    # 先剔除已退市和未上市标的
    df_code = get_instrumentinfos(sec_types=SEC_TYPE_BOND_CONVERTIBLE, fields='symbol, listed_date, delisted_date', df=True)
    df_code['listed_date'] = df_code['listed_date'].apply(lambda x:x.date())
    df_code['delisted_date'] = df_code['delisted_date'].apply(lambda x:x.date())
    all_convbond = [code for code in df_code[(df_code['listed_date']<=date)&(df_code['delisted_date']>date)].symbol.to_list()]
    return all_convbond


def on_backtest_finished(context, indicator):
    print('*'*50)
    print('回测已完成，请通过右上角“回测历史”功能查询详情。')

    
if __name__ == '__main__':
    '''
        strategy_id策略ID,由系统生成
        filename文件名,请与本文件名保持一致
        mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST
        token绑定计算机的ID,可在系统设置-密钥管理中生成
        backtest_start_time回测开始时间
        backtest_end_time回测结束时间
        backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
        backtest_initial_cash回测初始资金
        backtest_commission_ratio回测佣金比例
        backtest_slippage_ratio回测滑点比例
    '''
    run(strategy_id='b5f468f9-fb80-11ec-95c6-f46b8c02346f',
        filename='main.py',
        mode=MODE_BACKTEST,
        token='47ca47f849b3a0f66ec0f7013bb56bb667d63a70',
        backtest_start_time='2022-06-01 08:00:00',
        backtest_end_time='2022-07-21 15:30:00',
        backtest_adjust=ADJUST_PREV,
        backtest_initial_cash=200000,
        backtest_commission_ratio=0.0001,
        backtest_slippage_ratio=0.0001)